Skip to content

fix(intl): #5581 — NumberFormat option read order, useGrouping/roundingIncrement validation, and format accessor#5728

Merged
proggeramlug merged 3 commits into
mainfrom
fix/5581-numberformat-intl
Jun 27, 2026
Merged

fix(intl): #5581 — NumberFormat option read order, useGrouping/roundingIncrement validation, and format accessor#5728
proggeramlug merged 3 commits into
mainfrom
fix/5581-numberformat-intl

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Partially addresses #5581 (test262 intl402 NumberFormat gap). Aligns the Intl.NumberFormat constructor option handling and the format accessor with ECMA-402, closing 10 test262 cases with no regressions.

Changes

SetNumberFormatDigitOptions / CreateNumberFormat (number_format_options.rs)

  • Read localeMatcher first (it was never read at all), and keep the exact GetOption order the spec asserts — roundingIncrement, roundingMode, roundingPriority are now read in sequence instead of roundingPriority being read ahead of significant-digit options.
  • roundingIncrement is ToNumber-coerced (so { valueOf } and string options work) and validated against the sanctioned increment set {1,2,5,…,5000} rather than a bare [1,5000] range. A non-1 increment now requires the default fraction-digit rounding type (TypeError for significant digits / non-auto priority) with maximumFractionDigits == minimumFractionDigits (RangeError otherwise) — matching Node.
  • useGrouping now follows GetStringOrBooleanOption precisely: boolean true"always", any ToBoolean-false value (false/0/null/"") → false, the strings "true"/"false" fall back to the default, and only "min2"/"auto"/"always" are otherwise accepted.

format accessor (intl.rs, number_format.rs)

  • Intl.NumberFormat.prototype.format is now a getter (name "get format", length 0) returning the instance's bound format function (name "", length 1) — the ECMA-402 [[BoundFormat]] shape. Instances keep an own bound format for the hot dispatch path (native objects resolve methods from own props), and the prototype getter is reflection-correct.
  • install_constructor gained a getters slice; only NumberFormat populates it.

Tests fixed (test262 @ 4249661, Node v26.3.0)

  • constructor-option-read-order (read order now matches; full pass still needs Proxy-trap support in native option reads — see below), constructor-options-throwing-getters
  • constructor-roundingIncrement, constructor-roundingIncrement-invalid
  • test-option-useGrouping, test-option-useGrouping-extended
  • prototype/format/{length,name,prop-desc,no-instanceof,format-function-name}

NumberFormat subset: 106 → 121 passing, 0 regressions. The existing Intl node-suite differential is unchanged.

Not in scope (remaining #5581 work)

  • constructor-option-read-order via propertyBagObserver uses a Proxy; Perry's native option reads bypass Proxy get/has traps (validated here with plain getters instead).
  • Locale-list coercion (ToObject / array-like / HasProperty), formatRange/formatRangeToParts, currency sign placement, and CLDR-dependent locales (de/ja/ko/zh, non-Latin numbering systems).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Updated Intl.NumberFormat so prototype.format behaves like an accessor, returning the correctly bound formatter function for the instance.
    • Improved handling of rounding-related options with stricter ECMA-402 compliant parsing.
  • Bug Fixes

    • Fixed localeMatcher to be read in the correct GetOption order, preserving expected deterministic behavior.
    • Corrected useGrouping option coercion and validation.
    • Tightened roundingIncrement/roundingMode/roundingPriority and related fraction digit constraints so invalid combinations consistently throw.

…ngIncrement validation, and format accessor

Closes 10 test262 intl402/NumberFormat cases by aligning the constructor's
GetOption sequence and the `format` accessor shape with ECMA-402.

SetNumberFormatDigitOptions / CreateNumberFormat:
- Read `localeMatcher` first (it was never read), then keep the exact GetOption
  order the spec asserts: roundingIncrement, roundingMode, roundingPriority now
  read in sequence instead of roundingPriority jumping ahead of the others.
  (constructor-option-read-order, constructor-options-throwing-getters)
- `roundingIncrement` is ToNumber-coerced (so `{ valueOf }` / string options
  work) and validated against the sanctioned increment set rather than a bare
  [1, 5000] range; a non-1 increment now requires the default fraction-digit
  rounding type (TypeError otherwise) with maxFrac == minFrac (RangeError
  otherwise). (constructor-roundingIncrement[-invalid])
- `useGrouping` follows GetStringOrBooleanOption exactly: boolean true → "always",
  any ToBoolean-false value (false/0/null/"") → false, the strings "true"/"false"
  fall back to the default, and only "min2"/"auto"/"always" are otherwise valid.
  (test-option-useGrouping[-extended])

format accessor:
- `Intl.NumberFormat.prototype.format` is now a getter (name "get format",
  length 0) returning the instance's bound format function (name "", length 1),
  matching the ECMA-402 [[BoundFormat]] shape. Instances keep an own bound
  `format` for the hot dispatch path. (format/{length,name,prop-desc,
  no-instanceof,format-function-name})
- install_constructor gained a getters slice; only NumberFormat uses it.

Verified against test262 @ 4249661 with Node v26.3.0: NumberFormat subset goes
from 106 to 121 passing with no regressions; no parity changes in the existing
Intl node-suite differential.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ce00bd96-e000-4b94-8ceb-c8c9f22baf24

📥 Commits

Reviewing files that changed from the base of the PR and between 0a0a97f and dd70926.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/intl.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-runtime/src/intl.rs

📝 Walkthrough

Walkthrough

The PR updates Intl.NumberFormat so format is exposed through a getter, adjusts bound format instance setup, and revises option parsing for useGrouping, localeMatcher, and roundingIncrement.

Changes

Intl.NumberFormat wiring and options

Layer / File(s) Summary
Bound format instance setup
crates/perry-runtime/src/intl.rs
Intl.NumberFormat instance creation captures the bound format closure pointer, clears its native name when present, and stores it in a hidden slot; install_bound_instance_function now returns the created closure pointer.
Prototype format getter wiring
crates/perry-runtime/src/intl.rs, crates/perry-runtime/src/intl/number_format.rs
install_constructor accepts getter entries and installs getter-only prototype properties, and Intl.NumberFormat.prototype.format now resolves through number_format_format_getter_thunk.
useGrouping option resolution
crates/perry-runtime/src/intl.rs
get_use_grouping_option changes falsy-input handling and string matching for "true" and "false".
roundingIncrement option handling
crates/perry-runtime/src/intl/number_format_options.rs
configure_number_format reads localeMatcher early, SetNumberFormatDigitOptions uses roundingIncrement, and the helper and validation update stored rounding fields and constraints.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PerryTS/perry#5405: Also changes crates/perry-runtime/src/intl.rs constructor wiring in the shared Intl.* namespace path.
  • PerryTS/perry#5512: Also changes install_constructor in crates/perry-runtime/src/intl.rs.
  • PerryTS/perry#5605: Also changes Intl.NumberFormat option parsing in crates/perry-runtime/src/intl.rs.

Poem

I hopped through Intl by moonlit code,
And format now sits on a getter road.
My whiskers twitched at rounding’s tune,
While grouping rules danced by the moon.
🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main Intl.NumberFormat changes and matches the pull request scope.
Description check ✅ Passed It has a summary, concrete changes, and an issue reference, but the template's dedicated test plan/checklist sections are not fully filled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/5581-numberformat-intl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/intl.rs`:
- Around line 811-818: The NumberFormat bound format state is currently exposed
through the public own "format" property, which lets user mutations or deletion
affect [[BoundFormat]] and the prototype getter behavior. Update the
NumberFormat initialization and accessor path in intl.rs so
install_bound_instance_function no longer uses the public property as the
backing store; instead place the bound closure in an internal/hidden slot, have
number_format_format_getter_thunk return that slot, and keep the public "format"
property separate from the cached bound function.

In `@crates/perry-runtime/src/intl/number_format_options.rs`:
- Around line 184-198: In the number-format option validation logic inside
NumberFormatOptions, apply the roundingIncrement default-width adjustment before
the fraction-digit checks. When rounding_increment is not 1, ensure the resolved
minimumFractionDigits and maximumFractionDigits are forced to the same default
fraction width (so decimal/unit defaults become 0/0 instead of 0/3) before the
existing has_sd, rounding_priority, and equality validations run.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 693e9a80-ce58-47c7-9292-412289b042db

📥 Commits

Reviewing files that changed from the base of the PR and between 9fb1940 and 9e1aa32.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/intl.rs
  • crates/perry-runtime/src/intl/number_format.rs
  • crates/perry-runtime/src/intl/number_format_options.rs

Comment thread crates/perry-runtime/src/intl.rs
Comment thread crates/perry-runtime/src/intl/number_format_options.rs
Ralph and others added 2 commits June 27, 2026 01:32
…o fmt

CodeRabbit review follow-up:
- The `format` getter now reads the bound format function from a hidden
  KEY_NF_BOUND_FORMAT slot instead of the public own `format` property, so
  `nf.format = 1` / `delete nf.format` can no longer corrupt what
  `Object.getOwnPropertyDescriptor(proto, "format").get.call(nf)` returns. The
  own `format` property is retained only for the native dispatch fast path.
- Applied `cargo fmt`.

(CodeRabbit's second finding — that `roundingIncrement !== 1` should resolve the
default fraction width to 0/0 instead of throwing — does not match Node v26:
`new Intl.NumberFormat("en", { roundingIncrement: 5 })` and
`{ roundingIncrement: 5, maximumFractionDigits: 2 }` both throw RangeError there,
matching this implementation.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug proggeramlug merged commit 9b6d12c into main Jun 27, 2026
15 checks passed
@proggeramlug proggeramlug deleted the fix/5581-numberformat-intl branch June 27, 2026 08:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant